/*
* Copyright 2008 the original author or authors.
* 
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* 
* http://www.apache.org/licenses/LICENSE-2.0
* 
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ 

package com.vtls.oai.service.providers;

import java.text.SimpleDateFormat;
import com.vtls.oai.domain.MetadataFormat;
import com.vtls.oai.domain.Record;
import com.vtls.oai.domain.SetMembership;
import com.vtls.oai.service.exceptions.BadArgumentException;
import com.vtls.oai.service.exceptions.BadResumptionTokenException;
import com.vtls.oai.service.exceptions.BadVerbException;
import com.vtls.oai.service.exceptions.CannotDisseminateFormatException;
import com.vtls.oai.service.exceptions.IdDoesNotExistException;
import com.vtls.oai.service.exceptions.NoMetadataFormatsException;
import com.vtls.oai.service.exceptions.NoRecordsMatchException;
import com.vtls.oai.service.exceptions.NoSetHierarchyException;
import com.vtls.oai.service.exceptions.ProtocolException;
import com.vtls.oai.service.exceptions.ServerException;
import com.vtls.oai.service.Provider;
import com.vtls.oai.service.Session;
import java.io.InputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.dom4j.util.XMLErrorHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;

public class SolrProvider implements Provider
{
   private Log log = LogFactory.getLog(getClass());	

   private Map<String, String> metadataMapping = new HashMap<String, String>();
   private Map<String, String> sets = new HashMap<String, String>();
   private MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();

   private HttpClient httpClient = null;
   private SolrServer solrServer = null;
   private String fedoraUrl = null;
   private String query = null;

   private static int HTTP_CONNECTIONS = 128;
   private static String DEFAULT_FEDORA_URL = "http://localhost:8080/fedora";

   /* Constructors
   ***************************************************************************/
   
   public SolrProvider()
   {
      this.setFedoraUrl(DEFAULT_FEDORA_URL);
      
      final HttpConnectionManagerParams parameters = new HttpConnectionManagerParams();
      parameters.setStaleCheckingEnabled(false);
      parameters.setDefaultMaxConnectionsPerHost(HTTP_CONNECTIONS);
      parameters.setMaxTotalConnections(HTTP_CONNECTIONS * 2);

      connectionManager.setParams(parameters);

      httpClient = new HttpClient(connectionManager);
      httpClient.getParams().setConnectionManagerTimeout(6000);
   }
   
   /* Inner Classes
   ***************************************************************************/
   
   public class Memento
   {
      private int position = 0;
      
      public void setPosition(final int position)
      {
         this.position = position;
      }
      
      public int getPosition()
      {
         return this.position;
      }
   }
   
   /* Accessors/Mutators
   ***************************************************************************/

   public void setFedoraUrl(final String fedoraUrl)
   {
      this.fedoraUrl = fedoraUrl;
   }
   
   public String getFedoraUrl()
   {
      return this.fedoraUrl;
   }
   
   public void setQuery(final String query)
   {
      this.query = query;
   }
   
   public String getQuery()
   {
      return this.query;
   }
   
   public void setSolrServer(final SolrServer solrServer)
   {
      this.solrServer = solrServer;
   }
   
   public SolrServer getSolrServer()
   {
      return this.solrServer;
   }

   public Map<String, String> getSets()
   {
      return this.sets;
   }

   public void setSets(final Map<String, String> sets)
   {
      this.sets = sets;
   }

   public Map<String, String> getMetadataMapping()
   {
      return this.metadataMapping;
   }

   public void setMetadataMapping(final Map<String, String> metadataMapping)
   {
      this.metadataMapping = metadataMapping;
   }

   /* Provider Methods
   ***************************************************************************/

   public List<SetMembership> listSets() throws NoSetHierarchyException
   {
      final List<SetMembership> setMemberships = new ArrayList<SetMembership>();
      
      for(Map.Entry<String, String> entry : getSets().entrySet())
      {
         final SetMembership setMembership = new SetMembership();
         setMembership.setSetSpec(entry.getKey());
         setMembership.setSetName("Includes items matching the condition '" + entry.getValue() + "'.");
         // TODO: setMembership.setSetDescription();
         setMemberships.add(setMembership);
      }
      
      if(setMemberships.size() == 0)
         throw new NoSetHierarchyException();
         
      return setMemberships;
   }

   public String[] getMetadataPrefixes(final String identifier) throws IdDoesNotExistException
   {
      // TODO: Throw IdDoesNotExistException if the Object doesn't exist.
      
      String[] matchingPrefixes = new String[]{};

      final Set<String> metadataPrefixes = getMetadataMapping().keySet();
      for(String metadataPrefix : metadataPrefixes)
      {
         final String datastreamID = getMetadataMapping().get(metadataPrefix);

         if(StringUtils.isEmpty(datastreamID))
         {
            log.warn("No mapping from '" + metadataPrefix + "' to a datastream ID for '" + identifier + "'.");
            return null;
         }

         final StringBuffer downloadUrl = new StringBuffer();
         downloadUrl.append(this.getFedoraUrl()).append("/get/");
         downloadUrl.append(identifier).append("/").append(datastreamID);

         final HeadMethod headMethod = new HeadMethod(downloadUrl.toString());

         try
         {
            if(200 == httpClient.executeMethod(headMethod))
               matchingPrefixes = (String[])ArrayUtils.add(matchingPrefixes, metadataPrefix);
         }
         catch(IOException e)
         {
            log.error("There was a problem checking '" + downloadUrl.toString() + "'.", e);
         }
         finally
         {
            headMethod.releaseConnection();
         }
      }
      
      return matchingPrefixes;
   }

   public Record getRecord(final String identifier, final MetadataFormat metadataFormat) throws CannotDisseminateFormatException, IdDoesNotExistException
   {
      final Record record = new Record();

      final SolrQuery query = new SolrQuery();
      query.setQuery("identifier:\"" + identifier + "\"");

      try
      {
         final QueryResponse response = this.solrServer.query(query);
         final SolrDocumentList documents = response.getResults();

         if(documents.getNumFound() == 0)
            throw new IdDoesNotExistException();

         final SolrDocument document = documents.iterator().next();

         // TODO: Duplicated from listRecords.
         final Object value = document.getFirstValue("identifier"); // TODO: Configurable.
         if(value == null)
            throw new IdDoesNotExistException();

         // Identifier.
         record.setIdentifier(value.toString());

         // Date. // TODO: Configurable.
         if(document.getFirstValue("lastModifiedDate") != null)
            record.setDate(document.getFirstValue("lastModifiedDate").toString());

         // Deleted // TODO: Configurable.
         record.setDeleted(document.getFirstValue("isDeleted") != null);
      
         // Sets.
         for(String name : getSets().keySet())
         {
            query.setQuery("identifier:\"" + value.toString() + "\" AND " + getSets().get(name));

            final QueryResponse setResponse = this.solrServer.query(query);
            final SolrDocumentList setDocuments = setResponse.getResults();

            if(setDocuments.getNumFound() > 0)
               record.addSet(name);
         }
         
         return record;
      }
      catch(SolrServerException e)
      {
         log.error("There was a problem with retrieving information from Solr.", e);
      }
      
      throw new IdDoesNotExistException();
   }
   
   public List<Record> listRecords(final Session session) throws CannotDisseminateFormatException, NoRecordsMatchException
   {
      if(this.solrServer == null)
         throw new IllegalStateException("SolrProvider.listRecords() expects a non-null SolrServer instance.");
         
      final List<Record> records = new ArrayList<Record>();
      
      Memento memento = null;
      if(session.getMemento() instanceof Memento)
         memento = (Memento)session.getMemento();
      else
         memento = new Memento();

      final SolrQuery query = new SolrQuery();
      query.setQuery(StringUtils.defaultIfEmpty(this.query, "*:*"));
      query.setRows(50); // TODO: Configurable.
      query.setStart(new Integer(memento.getPosition()));
      query.addSortField("lastModifiedDateSort", SolrQuery.ORDER.desc); // TODO: Configurable.
      
      // Dates.
      final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");
      if(session.getFrom() != null && session.getUntil() != null)
      {
         final String formattedFrom = dateFormat.format(session.getFrom());
         final String formattedUntil = dateFormat.format(session.getUntil());
         query.setQuery(query.getQuery() + " AND lastModifiedDateFacet:[" +  formattedFrom + " TO " + formattedUntil + "]");
      }
      else if(session.getFrom() != null)
      {
         final String formattedFrom = dateFormat.format(session.getFrom());
         query.setQuery(query.getQuery() + " AND lastModifiedDateFacet:[" +  formattedFrom + " TO 99999999999999999]");
      }
      else if(session.getUntil() != null)
      {
         final String formattedUntil = dateFormat.format(session.getUntil());
         query.setQuery(query.getQuery() + " AND lastModifiedDateFacet:[00000000000000000 TO " + formattedUntil + "]");
      }
      
      // Limit query to objects containing datastreams matching our metadataFormats.
      final String metadataPrefix = session.getMetadataPrefix();
      final String dsid = getMetadataMapping().get(metadataPrefix);
      query.setQuery(query.getQuery() + " AND datastream:" + dsid);

      try
      {
         final QueryResponse response = this.solrServer.query(query);
         final SolrDocumentList documents = response.getResults();

         if(documents.getNumFound() == 0)
            throw new NoRecordsMatchException();

         final int nextPosition = memento.getPosition() + documents.size();
         memento.setPosition(nextPosition);

         if(nextPosition >= documents.getNumFound())
            session.setSessionComplete(true);

         for(SolrDocument document : documents)
         {
            final Record record = new Record();
            final Object value = document.getFirstValue("identifier"); // TODO: Configurable.
            if(value == null)
               continue;

            // Identifier.
            record.setIdentifier(value.toString());

            // Date. // TODO: Configurable.
            if(document.getFirstValue("lastModifiedDate") != null)
               record.setDate(document.getFirstValue("lastModifiedDate").toString());

            // Deleted // TODO: Configurable.
            record.setDeleted(document.getFirstValue("isDeleted") != null);
            
            // Sets.
            for(String name : getSets().keySet())
            {
               query.setQuery("identifier:\"" + value.toString() + "\" AND " + getSets().get(name));
               log.info("Filtering on '" + query.getQuery() + "'.");

               final QueryResponse setResponse = this.solrServer.query(query);
               final SolrDocumentList setDocuments = setResponse.getResults();

               if(setDocuments.getNumFound() > 0)
                  record.addSet(name);
            }

            records.add(record);
         }
      }
      catch(SolrServerException e)
      {
         log.error("There was a problem with retrieving information from Solr.", e);
      }
      finally
      {
         session.setMemento(memento);
      }
       
      return records;
   }
   
   public byte[] getRecordMetadata(final String identifier, final MetadataFormat metadataFormat) throws IOException, IdDoesNotExistException, CannotDisseminateFormatException
   {
      // Check for a Datastream ID.
      final String metadataPrefix = metadataFormat.getMetadataPrefix();
      final String datastreamID = getMetadataMapping().get(metadataPrefix);
      
      if(StringUtils.isEmpty(datastreamID))
      {
         log.warn("No mapping from '" + metadataPrefix + "' to a datastream ID for '" + identifier + "'.");
         return null;
      }

      final StringBuffer downloadUrl = new StringBuffer();
      downloadUrl.append(this.getFedoraUrl()).append("/get/");
      downloadUrl.append(identifier).append("/").append(datastreamID);
   
      final GetMethod getMethod = new GetMethod(downloadUrl.toString());
      try
      {
         httpClient.executeMethod(getMethod);
   
         final InputStream inputStream = getMethod.getResponseBodyAsStream();
         if(inputStream != null)
         {
            final SAXReader reader = new SAXReader();
            final OutputFormat format = OutputFormat.createCompactFormat();
            format.setSuppressDeclaration(true);

            try
            {
               final StringWriter stringWriter = new StringWriter();

               final XMLWriter writer = new XMLWriter(stringWriter, format);
               writer.write(reader.read(inputStream));

               IOUtils.closeQuietly(inputStream);
               return IOUtils.toByteArray(stringWriter.toString());
            }
            catch(Exception e)
            {
               log.error("There was a problem with the XML content from URL '" + downloadUrl.toString() + "'.", e);
            }
         }
         else
            log.warn("The stream was empty for URL '" + downloadUrl.toString() + "'.");
      }
      finally
      {
         getMethod.releaseConnection();
      }
      
      return null;
   }
   
   /* Local Methods
   ***************************************************************************/

   public void addSet(final String name, final String query)
   {
      if(name == null || query == null)
         throw new IllegalArgumentException("SolrProvider.addSet() expects non-null parameters.");

      this.sets.put(name, query);
   }

   public void removeSet(final String name)
   {
      this.sets.remove(name);
   }
   
   public void clearSets()
   {
      this.sets.clear();
   }
}
